
#include <QMessageBox>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QListWidgetItem>
#include <QFile>
#include <QFileDialog>
#include <QDesktopWidget>
#include <stdio.h>
#include <stdarg.h>
#include "loginwindow.h"
#include "downclient.h"
#include "ui_downclient.h"
#include "configfile.h"
#include "filedata_t.h"
#include "operate/download.h"

DownClient::DownClient(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::DownClient)
{
    ui->setupUi(this);
    this->init();
}

DownClient::~DownClient()
{
    this->deinit();
    delete ui;
}

DownClient::Status DownClient::heartbeatStart()
{
    this->heartbeatThread->start();
    return NO_ERROR;
}

DownClient::Status DownClient::reloadConfig()
{
    qDebug() << "DownClient::" << __FUNCTION__;
    if(this->settings != nullptr) {
        delete this->settings;
        this->settings = new ConfigFile(QCoreApplication::applicationDirPath() + "/" + DEFAULT_FILENAME);
    } else {
        this->settings = new ConfigFile(QCoreApplication::applicationDirPath() + "/" + DEFAULT_FILENAME);
    }
    this->setWindowTitle(QString("%1 - %2").
                         arg("File Isolator"/*this->windowTitle()*/).
                         arg(this->settings->value(USER_NAME).toString()));
    this->currentPath = "/";
    LOG("%s Login succeed.", this->settings->value(USER_NAME).toString().toLatin1().data());
    return NO_ERROR;
}

DownClient::Status DownClient::init()
{
    qDebug() << "DownClient::" << __FUNCTION__;

    /* read config file */
    this->settings = new ConfigFile(QCoreApplication::applicationDirPath() + "/" + DEFAULT_FILENAME);
    this->currentDownDir = this->settings->value(QString(USER_DOWN_DIR)).toString();
    this->currentUpDir = this->settings->value(QString(USER_UP_DIR)).toString();
    this->ui->checkBoxRunScript->setCheckState(this->settings->value(USER_RUN_SCRIPT).toBool()
                                               ? Qt::CheckState::Checked : Qt::CheckState::Unchecked);

    /* init network */
    this->heartbeatNetManager = new QNetworkAccessManager();
    connect(this->heartbeatNetManager,
            &QNetworkAccessManager::finished,
            this,
            &DownClient::heartbeatResponse);
    this->downloadNetManager = new QNetworkAccessManager();
    connect(this->downloadNetManager,
            &QNetworkAccessManager::finished,
            this,
            &DownClient::downloadFinishedTest);

    /* init heartbeat */
    this->isHeartbeating = false;
    this->heartbeatThread = new DownClientLoopThread(300, QString("heartbeatThread"));
    connect(this->heartbeatThread,
            &DownClientLoopThread::taskFuncRun,
            this,
            &DownClient::heartbeat);
    this->currentPath = "/";
    this->ui->labelCurrentPath->setText(this->currentPath);
    this->heartbeatTimer = new QTimer(this);
    connect(this->heartbeatTimer,
            &QTimer::timeout,
            this,
            &DownClient::heartbeatTimeout);
    this->heartbeatTimer->setSingleShot(true);

    /* init downlaod class */
    this->downloadTask = new Download();
    connect(this->downloadTask,
            &Download::finished,
            this,
            &DownClient::downloadFinished);
    this->downloadProcess = new QProcess(this);
    connect(this->downloadProcess,
            &QProcess::readyRead,
            this,
            &DownClient::downloadProcessReadData);
    connect(this->downloadProcess,
            QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
            this,
            &DownClient::downloadProcessFinished);

    /* init upload class */
    this->uploadTask = new Upload();
    connect(this->uploadTask,
            &Upload::finished,
            this,
            &DownClient::uploadFinished);

    /* init PathDelete class */
    this->deleteTask = new PathDelete();
    connect(this->deleteTask,
            &PathDelete::finished,
            this,
            &DownClient::deleteFinished);

    /* init event filter */
//    this->installEventFilter(this);
    this->ui->pushButtonDownload->installEventFilter(this);

    /* init MainWindow widget */
    this->loginWindow = new LoginWindow(this);
#ifndef DEBUG_MAIN_WINDOWS /* 仅用于调试主窗口，发布时记得注释掉，否则没有登录串口 */
    this->sourceCentralWidget = this->takeCentralWidget();
    this->setCentralWidget(this->loginWindow->centralWidget());
    /* 改800x400登录窗口然后居中 */
    int currentScreen = QApplication::desktop()->screenNumber(this);//获取程序所在的屏幕编号
    QRect rect = QApplication::desktop()->screenGeometry(currentScreen);//获取程序所在屏幕尺寸
    qDebug("desktop currentScreen:%d w:%d h:%d", currentScreen, rect.width(), rect.height());
    this->sourceWindowRect.setRect((rect.width() - this->geometry().width()) / 2,
                                   (rect.height() - this->geometry().height()) / 2,
                                   this->geometry().width(), this->geometry().height());
    this->setGeometry((rect.width() - this->loginWindow->D_Width) / 2,
                      (rect.height() - this->loginWindow->D_Height) / 2,
                      this->loginWindow->D_Width, this->loginWindow->D_Height);
    qDebug() << this->sourceWindowRect;
#endif

    /* 初始化 下载按钮 悬停与长按用到的定时器 */
    this->pushBtnDownCursorEnterTimer = new QTimer(this);
    connect(this->pushBtnDownCursorEnterTimer,
            &QTimer::timeout,
            this,
            &DownClient::pushBtnDownCursorEnterTimerTimeout);
    this->pushBtnDownCursorPressedTimer = new QTimer(this);
    connect(this->pushBtnDownCursorPressedTimer,
            &QTimer::timeout,
            this,
            &DownClient::pushBtnDownCursorPressedTimerTimeout);

    /* init TextEditLog widget */
    this->ui->textEditLog->document()->setMaximumBlockCount(999);

    LOG("heartbeat interval %ds", this->heartbeatThread->getIntervalSeconds());

    // 4. auto start check file is latest?
    return DownClient::NO_ERROR;
}

DownClient::Status DownClient::deinit()
{
    delete this->settings;
    delete this->heartbeatNetManager;
    if(this->downloadTask != nullptr) {
        delete this->downloadTask;
        this->downloadTask = nullptr;
    }
    if(this->uploadTask != nullptr) {
        delete this->uploadTask;
        this->uploadTask = nullptr;
    }
    return DownClient::NO_ERROR;
}

void DownClient::heartbeat()
{
    qDebug() << "DownClient::" << __FUNCTION__;

    if(this->isHeartbeating == false) {
        this->isHeartbeating = true;
    } else {
        qDebug() << "DownClient::" << __FUNCTION__ << "The conversation is in progress. "
                    "Therefore, new sessions cannot be processed.";
        return ;
    }

    QString userName = this->settings->value(USER_NAME).toString();
    QString userPassword = this->settings->value(USER_PASSWORD).toString();
    QString userPhpSessId = this->settings->value(USER_PHPSESSID).toString();
    if(userName.length() != 0 && userPassword.length() != 0 && userPhpSessId.length() != 0) {
        qDebug() << "DownClient::" << __FUNCTION__ << USER_NAME " : " << userName;
        qDebug() << "DownClient::" << __FUNCTION__ << USER_PASSWORD " : " << userPassword;
        qDebug() << "DownClient::" << __FUNCTION__ << USER_PHPSESSID " : " << userPhpSessId;
    } else {
        qDebug() << "DownClient::" << __FUNCTION__ <<
                    "init error, name or password or phpsessionID is null.";
        QMessageBox::critical(this, "Init Error", "Name or Password or PHPSESSID is null.");
        return ;
    }

    /* 构建URL请求 */
    /* http://filegeli.timechicken.cc/index.php?explorer/pathList&path=/ */
    QString updateUrl;
    updateUrl += this->settings->value(USER_WEB_URL).toString();
    updateUrl += QString("/index.php?explorer/pathList&path=%1").arg(this->currentPath);
    qDebug() << "DownClient::" << __FUNCTION__ << "URL:" << updateUrl;
    QNetworkRequest request = QNetworkRequest(QUrl(updateUrl));
    request.setRawHeader(QByteArray("Cookie"),
                         QString("PHPSESSID=%1; secros_user_language=zh_CN; secros_name=%2")
                         .arg(userPhpSessId)
                         .arg(this->settings->value(USER_NAME).toString())
                         .toUtf8()
                         );
    request.setHeader(QNetworkRequest::UserAgentHeader,
                      "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)"
                      " Chrome/104.0.0.0 Safari/537.36");
    QList<QByteArray> requestHeadersList = request.rawHeaderList();
    foreach(QByteArray i, requestHeadersList) {
        qDebug() << i << request.rawHeader(i);
    }
    this->heartbeatNetManager->get(request);
    if(this->heartbeatTimer->isActive()) {
        this->heartbeatTimer->stop();
    }
    this->heartbeatTimer->start(10 * 1000); // 10s
    qDebug() << "DownClient::heartbeat heartbeat request done.";
}

void DownClient::heartbeatTimeout()
{
    qDebug() << "DownClient::" << __FUNCTION__;
    /* 230604出现异常后弹出的错误框会阻塞，未确认的场景下heartbeatThread不会stop，
     * 会导致一直弹框，所以这里需要stop掉heartbeatThread，否则会一直弹框 */
    this->heartbeatTimer->stop(); /* run 1s after */
    this->heartbeatThread->wait();
    this->heartbeatThread->stop();
    QMessageBox::critical(this, "ERROR", "Session expired, please logon again.");
    this->returnLoginWindow();
}

DownClient::Status DownClient::returnLoginWindow()
{
    qDebug() << "DownClient::" << __FUNCTION__;
//    qDebug() << "debug env, so return .";
//    return NO_ERROR;

    LoginWindow *loginWindow = this->loginWindow;
    this->setWindowTitle(QString("File Isolator"));
    this->ui->listWidgetDir->clear();

    this->sourceCentralWidget = this->takeCentralWidget();
    this->sourceWindowRect = this->geometry();
    this->setCentralWidget(this->loginWindow->centralWidget());
    /* 改800x400登录窗口然后居中 */
    int currentScreen = QApplication::desktop()->screenNumber(this);//获取程序所在的屏幕编号
    QRect rect = QApplication::desktop()->screenGeometry(currentScreen);//获取程序所在屏幕尺寸
    qDebug("desktop currentScreen:%d w:%d h:%d", currentScreen, rect.width(), rect.height());
    this->setGeometry((rect.width() - this->loginWindow->D_Width) / 2,
                      (rect.height() - this->loginWindow->D_Height) / 2,
                      this->loginWindow->D_Width, this->loginWindow->D_Height);

    loginWindow->resetSignalAndSlot();

    /* 230604出现异常后弹出的错误框会阻塞，未确认的场景下heartbeatThread不会stop，
     * 会导致一直弹框 */
//    this->heartbeatThread->stop();
//    this->heartbeatThread->wait();
    return NO_ERROR;
}

void DownClient::heartbeatResponse(QNetworkReply *reply)
{
    qDebug() << "DownClient::" << __FUNCTION__;

    if(this->isHeartbeating == true) {
        this->isHeartbeating = false;
    }

    if(this->heartbeatTimer->isActive()) {
        this->heartbeatTimer->stop();
    } else {
        qDebug() << "DownClient::" << __FUNCTION__ << "The session has timed out and will not be processed.";
        return ;
    }
    /* 过滤请求错误 */
    if(reply->error() != QNetworkReply::NoError) {
        this->heartbeatThread->stop();
        this->heartbeatThread->wait(); /* max to wait 1s */
        QMessageBox::critical(this, "Heartbeat Error", reply->errorString());
        this->returnLoginWindow();
        return ;
    }

    /* 判断响应数据 */
    QVariant statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
    /* 如果解析响应状态码错误或者状态码!=200 */
    qDebug() << "requestFinished Response statusCode:" << statusCode.toInt();
    if(!statusCode.isValid() || statusCode.toInt() != 200) {
        if(statusCode.toInt() == 302) {
            this->heartbeatThread->stop();
            this->heartbeatThread->wait(); /* max to wait 1s */
            QMessageBox::critical(this, "Response Error", "PHPSESSID overdue, please loggin in again");
            this->returnLoginWindow();
            return ;
        }
        QVariant reason =  reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute);
        if(reason.isValid())
            QMessageBox::critical(this, "Response Error", reason.toString());
        else
            QMessageBox::critical(this, "Response Error", "响应错误");
        this->heartbeatThread->stop();
        this->heartbeatThread->wait(); /* max to wait 1s */
        this->returnLoginWindow();
        return ;
    }

    /* 响应成功，开始解析数据 */
//    QList<QByteArray> replyHeadersList = reply->rawHeaderList();
//    foreach(QByteArray i, replyHeadersList) {
//        qDebug() << i << reply->rawHeader(i);
//    }
//    qDebug() << "ResponseData: \n" << QString(reply->readAll());
    if(this->parseResponseDataToFileDataList(reply->readAll()) != DownClient::NO_ERROR) {
        this->heartbeatThread->stop();
        this->heartbeatThread->wait(); /* max to wait 1s */
        QMessageBox::critical(this, "Error", "Json parse error");
        this->returnLoginWindow();
        return ;
    }
    if(this->updateUIByFileDataList() != DownClient::NO_ERROR) {
        this->heartbeatThread->stop();
        this->heartbeatThread->wait(); /* max to wait 1s */
        QMessageBox::critical(this, "Error", "FileDataList no data.");
        this->returnLoginWindow();
        return ;
    }
    this->heartbeatThread->resetCounter();

    LOG("%s heartbeat succeed.", this->settings->value(USER_NAME).toString().toLatin1().data());
    qDebug () << "DownClient::" << __FUNCTION__ << "Succeed.";
}

DownClient::Status DownClient::parseResponseDataToFileDataList(const QByteArray &jsonArray)
{
    qDebug().noquote() << "DownClient::" << __FUNCTION__ << "jsonArray : \n" << jsonArray;
    this->fileDataList.clear();
    QJsonParseError jsonParseError;

    QJsonDocument replyJsonDoc = QJsonDocument::fromJson(jsonArray, &jsonParseError);
    QJsonObject replyJsonObj = replyJsonDoc.object();
    if(jsonParseError.error != QJsonParseError::NoError) {
        qDebug() << "DownClient::" << __FUNCTION__ << "json parse error:" << jsonParseError.errorString();
        QMessageBox::critical(this, __FUNCTION__, jsonParseError.errorString());
        return JSON_ERROR;
    }
    qDebug().noquote() << "Json Parse Done Data:\n" << replyJsonDoc.toJson(QJsonDocument::Indented);
    QJsonObject dataJsonObj = replyJsonObj.value("data").toObject();
    /* 先解析文件类型 */
    QJsonArray filelistJsonArray = dataJsonObj.value("filelist").toArray();
    for(int i = 0; i < filelistJsonArray.size(); i++) {
        file_t fileData;
        QJsonObject fileJsonObj = filelistJsonArray.at(i).toObject();
        fileData.name = fileJsonObj.value("name").toString();
        fileData.path = fileJsonObj.value("path").toString();
        fileData.type = fileJsonObj.value("type").toString();
        fileData.isFile = true;
        fileData.mode = fileJsonObj.value("mode").toString();
        fileData.atime = fileJsonObj.value("atime").toVariant().toULongLong();
        fileData.ctime = fileJsonObj.value("ctime").toVariant().toULongLong();
        fileData.mtime = fileJsonObj.value("mtime").toVariant().toULongLong();
        fileData.is_readable = fileJsonObj.value("is_readable").toInt();
        fileData.is_writeable = fileJsonObj.value("is_writeable").toInt();
        fileData.size = fileJsonObj.value("size").toVariant().toULongLong();
        fileData.size_friendly = fileJsonObj.value("size_friendly").toString();
        fileData.ext = fileJsonObj.value("ext").toString();
        this->fileDataList.append(fileData);
    }
    /* 解析文件夹 */
    QJsonArray folderlist = dataJsonObj.value("folderlist").toArray();
    for(int i = 0; i < folderlist.size(); i++) {
        file_t folderData;
        QJsonObject folderJsonObj = folderlist.at(i).toObject();
        folderData.name = folderJsonObj.value("name").toString();
        folderData.path = folderJsonObj.value("path").toString();
        folderData.type = folderJsonObj.value("type").toString();
        folderData.isFile = false;
        folderData.mode = folderJsonObj.value("mode").toString();
        folderData.atime = folderJsonObj.value("atime").toVariant().toULongLong();
        folderData.ctime = folderJsonObj.value("ctime").toVariant().toULongLong();
        folderData.mtime = folderJsonObj.value("mtime").toVariant().toULongLong();
        folderData.is_readable = folderJsonObj.value("is_readable").toInt();
        folderData.is_writeable = folderJsonObj.value("is_writeable").toInt();\
        /* is folder, so default value */
        folderData.size = 0;
        folderData.size_friendly = "";
        folderData.ext = "";
        this->fileDataList.append(folderData);
    }
    qDebug() << "DownClient::" << __FUNCTION__ << "done.";
    return NO_ERROR;
}

DownClient::Status DownClient::updateUIByFileDataList()
{
    qDebug().noquote() << "DownClient::" << __FUNCTION__;
    this->ui->listWidgetDir->clear();

    for(int i = 0; i < this->fileDataList.size(); i++) {
        file_t fileData = this->fileDataList.at(i);
        QIcon icon(fileData.isFile ? ":/file.png" : ":/folder.png");
        QListWidgetItem *item = new QListWidgetItem(icon, fileData.name);
        this->ui->listWidgetDir->addItem(item);
    }
    this->ui->labelCurrentPath->setText(this->currentPath);

    qDebug() << "DownClient::" << __FUNCTION__ << "done.";
    return NO_ERROR;
}

void DownClient::on_listWidgetDir_itemDoubleClicked(QListWidgetItem *item)
{
    int line = this->ui->listWidgetDir->row(item);
    qDebug().noquote() << "DownClient::" << __FUNCTION__ << QString("ClickedLine:%1").arg(line);
    file_t fileData = this->fileDataList.at(line);
    if(fileData.isFile) {
        /* fileData is file */
        qDebug().noquote() << "DownClient::" << __FUNCTION__ <<
            QString("'%1' is file, so no entry file.").arg(fileData.name);
        this->on_pushButtonDownload_clicked();
    } else {
        /* fileData is folder */
        this->currentPath += fileData.name + "/";
        heartbeat();
    }
}

void DownClient::downloadFinished(int code, QString errorString)
{
    if(code != NO_ERROR) {
        QMessageBox::critical(this, "Download", errorString);
    } else {
//        QMessageBox::information(this, "Download", errorString);
        LOG("Download %s", errorString.toLatin1().data());
        this->down_done_script();
    }
}

void DownClient::downloadFinishedTest(QNetworkReply *reply)
{
    qDebug() << "DownClient::" << __FUNCTION__;
    /* 过滤请求错误 */
    if(reply->error() != QNetworkReply::NoError) {
        qDebug().noquote() << __FUNCTION__ << reply->errorString();
        return;
    }

    /* 判断响应数据 */
    QVariant statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
    /* 如果解析响应状态码错误或者状态码!=200 */
    qDebug() << "downloadResponse Response statusCode:" << statusCode.toInt();
    if(!statusCode.isValid() || statusCode.toInt() != 200) {
        QVariant reason =  reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute);
        if(reason.isValid())
            qDebug() << __FUNCTION__ << reason.toString();
        else
            qDebug() << __FUNCTION__ << "未知错误";
        return ;
    }

    QList<QByteArray> replyHeaderList = reply->rawHeaderList();
    for(int i = 0; i < replyHeaderList.size(); i++) {
        qDebug() << __FUNCTION__ << replyHeaderList.at(i) << reply->rawHeader(replyHeaderList.at(i));
    }

    /* save data to file */
    QFile file(QApplication::applicationDirPath() + "/download/config.txt");
    bool isOpen = file.open(QIODevice::WriteOnly);
    if(!isOpen) {
        qDebug() << "DownloadThread::" << __FUNCTION__ <<
                    QString("Open '%1' error:%2").arg(QApplication::applicationDirPath() + "/download/config.txt").arg(file.errorString());
    }
    int writeLen = file.write(reply->readAll());
    qDebug() << "DownloadThread::" << __FUNCTION__ <<
                QString("'%1' writed %2 bytes.").arg(QApplication::applicationDirPath() + "/download/config.txt").arg(writeLen);
    file.close();
    QMessageBox::information(this, "提示", "下载成功");
}

void DownClient::downloadProcessFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
    (void)exitStatus;
    qDebug().noquote() << "DownClient::" << __FUNCTION__ <<
                          QString("exitCode=%1").arg(exitCode);
    if(exitCode != 0 || exitStatus != QProcess::ExitStatus::NormalExit) {
        QMessageBox::critical(this, "下载完成", QString("出错 ErrorCode：%1").arg(exitCode));
        this->down_done_script();
    } else {
        QMessageBox::information(this, "提示", "下载成功");
    }
}

void DownClient::downloadProcessReadData()
{
    qDebug() << __FUNCTION__;
    qDebug() << this->downloadProcess->readAll();
}

void  DownClient::uploadFinished(int code, QString errorString)
{
    if(code != NO_ERROR) {
        QMessageBox::critical(this, "Download", errorString);
    } else {
        QMessageBox::information(this, "Download", errorString);
    }
}

void DownClient::deleteFinished(int code, QString errorString)
{
    this->heartbeat();
    if(code != NO_ERROR) {
        QMessageBox::critical(this, "Delete", errorString);
    } else {
        QMessageBox::information(this, "Delete", errorString);
    }
}

void DownClient::keyReleaseEvent(QKeyEvent *ev)
{
    qDebug() << "DownClient::" << __FUNCTION__ << "Key:" << ev->key();
    switch (ev->key()) {
    case Qt::Key_F5:
        this->heartbeat();
        break;
    case Qt::Key_Delete:
        this->on_pushButtonDelete_clicked();
        break;
    }
}

/*******************************************************
 * @brief 重写窗口类的所有鼠标触发事件，相当于一个控件事件过滤器
 * @param target
 * @param event
 * @return
 */
bool DownClient::eventFilter(QObject *target, QEvent *event)
{
//    qDebug("DownClient::%s target:%p event.type:%d", __FUNCTION__, target, event->type());

    /* 窗口被激活 */
    if(target == this) {
//        if(event->type() == QEvent::WindowActivate) {
//            qDebug("DownClient::%s QEvent::WindowActivate", __FUNCTION__);
//            this->heartbeat();
//        }
    }

    /* 下载按钮 事件 */
    else if(target == this->ui->pushButtonDownload) {
        if(event->type() == QEvent::Enter) {
            this->on_pushButtonDownload_CursorEnter();
        } else if (event->type() == QEvent::Leave) {
            this->on_pushButtonDownload_CursorLeave();
        }
    }

    // pass the event on to the parent class
    return QMainWindow::eventFilter(target, event);
}

/*******************************************************
 * @brief 获取主窗口的源CentralWidget指针
 * @return QWidget *
 */
QWidget *DownClient::getSourceCentralWidget()
{
    return this->sourceCentralWidget;
}

/*******************************************************
 * @brief 获取主窗口的源Rect参数
 * @return QrRect
 */
QRect DownClient::getSourceWindowRect()
{
    return this->sourceWindowRect;
}

/*******************************************************
 * @brief TextEditLog文本输出
 * @return 输出单行字符串的长度
 */
int DownClient::log(const char *format, ...)
{
    static char buffer[4096] = {0};
    va_list va_param;
    va_start(va_param, format);
    vsnprintf(buffer, sizeof(buffer) - 1, format, va_param);
    va_end(va_param);

    QString time_str = QDateTime::currentDateTime().toString(QString("yyyy/MM/dd HH:mm:ss:zzz "));

    QString log_str = time_str + buffer + '\n';
    /* https://blog.csdn.net/qq_37529913/article/details/109217353 */
    QTextCursor cursor = this->ui->textEditLog->textCursor();
    cursor.movePosition(QTextCursor::End);
    this->ui->textEditLog->setTextCursor(cursor);
    this->ui->textEditLog->insertPlainText(log_str);
    /* 追加完文本后再设置一次光标位置，防止输出多行时只能显示输出的第一行 */
    cursor.movePosition(QTextCursor::End);
    this->ui->textEditLog->setTextCursor(cursor);

    return log_str.length();
}
int DownClient::log(QtMsgType level, const char *format, ...)
{
    static char buffer[4096] = {0};
    va_list va_param;
    va_start(va_param, format);
    vsnprintf(buffer, sizeof(buffer), format, va_param);
    va_end(va_param);

    QString level_str;
    switch ((int)level) {
    case QtDebugMsg:    level_str = "[Debug]";      break;
    case QtInfoMsg:     level_str = "[Info]";       break;
    case QtWarningMsg:  level_str = "[Warning]";    break;
    case QtCriticalMsg: level_str = "[Critical]";   break;
    case QtFatalMsg:    level_str = "[Fatal]";      break;
    default:            level_str = "[Other]";      break;
    }

    QString time_str = QDateTime::currentDateTime().toString(QString("yyyy/MM/dd HH:mm:ss:zzz "));

    QString log_str = time_str + level_str + ' ' + buffer + '\n';
    /* https://blog.csdn.net/qq_37529913/article/details/109217353 */
    QTextCursor cursor = this->ui->textEditLog->textCursor();
    cursor.movePosition(QTextCursor::End);
    this->ui->textEditLog->setTextCursor(cursor);
    this->ui->textEditLog->insertPlainText(log_str);
    /* 追加完文本后再设置一次光标位置，防止输出多行时只能显示输出的第一行 */
    cursor.movePosition(QTextCursor::End);
    this->ui->textEditLog->setTextCursor(cursor);

    return log_str.length();
}
